package evs.location;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;

import org.hibernate.search.annotations.IndexedEmbedded;

public class EntityMap
{
    private final HashMap<String, Class<?>> _nameToClass = new HashMap<String, Class<?>>();
    private final HashMap<Class<?>, String> _classToName = new HashMap<Class<?>, String>();
    private final HashMap<Class<?>, Member> _idMap = new HashMap<Class<?>, Member>();
    private final HashMap<String, Class<?>> _pathToEntity = new HashMap<String, Class<?>>();
    private final HashMap<Class<?>, String[]> _fullTextFields = new HashMap<Class<?>, String[]>();

    public EntityMap( Set<Class<?>> classes )
    {
        for ( Class<?> c : classes )
        {
            assert c.isAnnotationPresent( Entity.class );

            String entityName = c.getSimpleName().toLowerCase();

            assert !_nameToClass.containsKey( entityName );

            _nameToClass.put( entityName, c );
            _classToName.put( c, entityName );

            if ( c.isAnnotationPresent( RESTful.class ) )
            {
                _configurePath( c );
                _configureFulltextIndexing( c );
            }

            for ( Field field : getFields( c ) )
            {
                if ( field.isAnnotationPresent( Id.class ) )
                {
                    _idMap.put( c, field );
                    break;
                }
            }
        }
    }

    public synchronized Class<?> getClassFor( String name )
    {
        return _nameToClass.get( name.toLowerCase() );
    }

    public synchronized Set<Class<?>> getEntityClasses()
    {
        return new HashSet<Class<?>>( _nameToClass.values() );
    }

    public synchronized Serializable convertId( Class<?> entityClass, String id )
    {
        assert this.getEntityClasses().contains( entityClass );

        for ( Field field : getFields( entityClass ) )
        {
            if ( field.isAnnotationPresent( Id.class ) )
            {
                return convertTo( id, field.getType() );
            }
        }

        return null;
    }

    private static Serializable convertTo( String value, Class<?> type )
    {
        try
        {
            return (Serializable) Converter.convertTo( value, type );
        }
        catch ( EvsException e )
        {
            return null;
        }
    }

    public synchronized List<Object> getObjectsInGraph( Object entity )
    {
        assert _nameToClass.values().contains( entity.getClass() );

        ArrayList<Object> objectsVisited = new ArrayList<Object>();

        try
        {
            _walkObjectGraph( entity, objectsVisited );
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }

        return objectsVisited;
    }

    public synchronized Serializable getId( Object o )
    {
        assert _nameToClass.containsValue( o.getClass() );

        for ( Field field : getFields( o.getClass() ) )
        {
            if ( field.isAnnotationPresent( Id.class ) )
            {
                try
                {
                    field.setAccessible( true );
                    return (Serializable) field.get( o );
                }
                catch ( Exception e )
                {
                    e.printStackTrace();
                }
            }
        }

        return null;
    }

    public static List<Field> getFields( Class<?> c )
    {
        List<Field> fields = new ArrayList<Field>();

        Field[] fieldsOfThisClass = c.getDeclaredFields();
        
        for ( int i = 0; i < fieldsOfThisClass.length; ++i )
            if ( ( fieldsOfThisClass[i].getModifiers() & Modifier.STATIC ) == 0 )
                fields.add( fieldsOfThisClass[i] );

        if ( c.getSuperclass() != null )
        {
            fields.addAll( getFields( c.getSuperclass() ) );
        }

        return fields;
    }

    public static Field getField( Class<?> c, String name )
    {
        try
        {
            return c.getDeclaredField( name );
        }
        catch ( SecurityException e )
        {
        }
        catch ( NoSuchFieldException e )
        {
            if ( c.getSuperclass() != null )
                return getField( c.getSuperclass(), name );
        }

        return null;
    }

    public synchronized Field getIdField( Class<?> entityClass )
    {
        return (Field) _idMap.get( entityClass );
    }

    public synchronized boolean hasIdElement( EntityDom dom )
    {
        Class<?> c = getClassFor( dom.name );
        Member idMember = _idMap.get( c );

        if ( idMember instanceof Field )
        {
            return dom.getChild( idMember.getName() ) != null;
        }
        else
        {
            assert idMember instanceof Method;

            String name = idMember.getName().substring( "get".length() );
            name = java.beans.Introspector.decapitalize( name );

            return dom.getChild( name ) != null;
        }
    }

    public synchronized Serializable getId( EntityDom dom )
    {
        assert hasIdElement( dom );

        Class<?> c = getClassFor( dom.name );
        Member idMember = _idMap.get( c );

        String idString;
        Class<?> idType;

        if ( idMember instanceof Field )
        {
            idString = dom.getChild( idMember.getName() ).value;
            idType = ( (Field) idMember ).getType();
        }
        else
        {
            assert idMember instanceof Method;

            String name = idMember.getName().substring( "get".length() );
            name = java.beans.Introspector.decapitalize( name );

            idString = dom.getChild( name ).value;
            idType = ( (Method) idMember ).getReturnType();
        }

        return convertTo( idString, idType );
    }

    public synchronized String getNameForClass( Class<?> entityClass )
    {
        return entityClass.getSimpleName();
    }

    public synchronized Class<?> getEntityForPath( String path )
    {
        return _pathToEntity.get( path );
    }

    public synchronized String[] getFulltextSearchFields( Class<?> entityClass )
    {
        return _fullTextFields.get( entityClass );
    }

    private void _configureFulltextIndexing( Class<?> c )
    {
        ArrayList<String> indexedFields = new ArrayList<String>();

        for ( Field f : getFields( c ) )
        {
            if ( f.isAnnotationPresent( org.hibernate.search.annotations.Field.class ) )
            {
                indexedFields.add( f.getName() );
            }

            if ( f.isAnnotationPresent( IndexedEmbedded.class ) )
            {
                if ( Collection.class.isAssignableFrom( f.getType() ) )
                {
                    ParameterizedType pt = (ParameterizedType) f.getGenericType();
                    Class<?> componentType = (Class<?>) pt.getActualTypeArguments()[0];

                    for ( Field subField : getFields( componentType ) )
                    {
                        if ( subField.isAnnotationPresent( org.hibernate.search.annotations.Field.class ) )
                        {
                            indexedFields.add( f.getName() + "." + subField.getName() );
                        }
                    }
                }
                else
                {
                    for ( Field subField : getFields( f.getType() ) )
                    {
                        if ( subField.isAnnotationPresent( org.hibernate.search.annotations.Field.class ) )
                        {
                            indexedFields.add( f.getName() + "." + subField.getName() );
                        }
                    }
                }
            }
        }

        _fullTextFields.put( c, indexedFields.toArray( new String[indexedFields.size()] ) );
    }

    private void _configurePath( Class<?> c )
    {
        RESTful annotation = c.getAnnotation( RESTful.class );

        String path;

        if ( !annotation.path().equals( "" ) )
        {
            path = annotation.path();
        }
        else
        {
            path = c.getSimpleName() + "s";
        }

        _pathToEntity.put( path, c );
    }

    private void _walkObjectGraph( Object current, List<Object> visited )
                    throws IllegalArgumentException, IllegalAccessException
    {
        if ( _objectVisited( current, visited ) )
            return;

        visited.add( current );

        // Search Fields
        for ( Field field : getFields( current.getClass() ) )
        {
            if ( field.isAnnotationPresent( Transient.class ) )
                continue;

            if ( ( field.getModifiers() & Modifier.TRANSIENT ) != 0 )
                continue;

            if ( field.getType().isAssignableFrom( Collection.class ) )
            {
                assert field.getType().getTypeParameters().length == 1;

                // Only follow entity references
                if ( !_nameToClass.containsValue( field.getType().getTypeParameters()[0] ) )
                    continue;

                Collection<?> collection = (Collection<?>) field.get( current );

                for ( Object next : collection )
                {
                    _walkObjectGraph( next, visited );
                }
            }
            else if ( field.getType().isArray() )
            {
                // Only follow entity references
                if ( !_nameToClass.containsValue( field.getType().getComponentType() ) )
                    continue;

                Object array = field.get( current );

                for ( int i = 0; i < Array.getLength( array ); ++i )
                {
                    Object next = Array.get( array, i );
                    _walkObjectGraph( next, visited );
                }
            }
            else
            {
                // Only follow entity references
                if ( !_nameToClass.containsValue( field.getType() ) )
                    continue;

                Object next = field.get( current );
                _walkObjectGraph( next, visited );
            }
        }
    }

    private boolean _objectVisited( Object o, List<Object> visited )
    {
        for ( Object o2 : visited )
        {
            if ( o == o2 )
                return true;
        }

        return false;
    }
}
